feat(testing): add skip-time virtual clock#523
Conversation
5c208f2 to
355edf1
Compare
346a870 to
2e9ba2b
Compare
9829546 to
71962f1
Compare
d2f04f2 to
c494657
Compare
71962f1 to
11103e3
Compare
c494657 to
c1b3e66
Compare
c1b3e66 to
c317391
Compare
|
rebased on main, no other change [edit:] ooh hang on CI failures 😕 , investigating |
|
seems |
907f424 to
089c4b8
Compare
|
changed:
Verification: 1353 testing-package tests pass, full workspace run 2883 passed, the 4 previously failing callback tests pass locally and in CI, mypy clean (119 files), hatch fmt --check clean. 3.13 integration test failure failing only on test_otel_logger_example_spans_in_xray, which is known pre-existing OTel X-Ray flk. |
089c4b8 to
da05414
Compare
| store if store is not None else InMemoryExecutionStore() | ||
| ) | ||
| self.poll_interval = poll_interval | ||
| self._clock: Clock = SkipClock() if skip_time else RealClock() |
There was a problem hiding this comment.
Would a shared SkipClock() between multiple executions cause issues?
There was a problem hiding this comment.
The only cross-execution effect is acceleration.
If execution A arms a skip past execution B's wait horizon, B's wait becomes due on its next wake instead of at its own arm. Since a single monotonic clock serves everything, it can only accelerate, never reorder.
What it can't do:
- Prematurely fire externally driven timers. Callback timeout/heartbeat, execution timeout, and invocation timeout are real call_later delays on the scheduler, so A skipping an hour of virtual time doesn't age B's 30s callback timeout.
- Distort history. Modeled durations come from scheduled_end - start, both stamped from the same clock, so they stay faithful regardless of who skipped what.
- The alternative (per-execution clocks) is worse: executions share one store and one history surface, so independently advancing clocks put cross-execution timestamps in different clock domains — the same class of bug as the CI failure this revision fixed, one level up.
- Add Clock protocol with RealClock and SkipClock implementations - SkipClock returns real time plus the accumulated skipped duration, so virtual time advances with real time between jumps and every read stays monotonic and distinct - One clock per runner, shared by the Executor, CheckpointProcessor, and all timestamp stamping, so history events form a single totally ordered clock domain - Drive wait and step-retry timers off the clock so modeled delays complete instantly under skip while history keeps modeled durations - Stamp operation, invocation, and execution start/end timestamps from the clock; reconstruct operations from event timestamps - Ban datetime.now via ruff TID251 outside clock.py so all time reads go through the clock - Default DurableFunctionTestRunner to skip_time=True; pass skip_time=False for real wall-clock timing - Remove the DURABLE_EXECUTION_TIME_SCALE env-var scaling
da05414 to
983e4bb
Compare
Description of changes:
Issue #, if available:
closes #437
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.